I have reverse engineered the way Microsoft Outlook Web Access uses Ajax to mark records read / unread under Internet Exploder and have added similar functionality to my Firefox Greasemonkey Outlook Web Access Extensions.
The code, also shown below, can be downloaded here.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Outlook Web Access Extensions | |
// @namespace http://www2.hawaii.edu/~dburger | |
// @description Extensions to using the bastard child Outlook Web Access | |
// ==/UserScript== | |
( | |
function() { | |
var UNREAD_IMG = '/exchweb/img/icon-msg-unread.gif'; | |
var READ_IMG = '/exchweb/img/icon-msg-read.gif'; | |
function walkTheDom(node, func) { | |
func(node); | |
node = node.firstChild; | |
while(node) { | |
walkTheDom(node, func); | |
node = node.nextSibling; | |
} | |
} | |
function getToolbarTable() { | |
var tables = document.getElementsByTagName('table'); | |
for (var i = 0; i < tables.length; i++) { | |
var table = tables[i]; | |
if (table.className === 'trToolbar') return table; | |
} | |
return null; | |
} | |
function getFindNamesForm() { | |
var forms = document.getElementsByTagName('form'); | |
return (forms.length == 1 && forms[0].name == 'galfind') ? forms[0] : null; | |
} | |
function getBaseHref() { | |
var bases = document.getElementsByTagName('base'); | |
return (bases) ? bases[0].href : null; | |
} | |
function addToolbarButton(toolbarTable, text, func) { | |
var row = toolbarTable.tBodies[0].rows[0]; | |
var newCell = row.insertCell(row.cells.length - 1); | |
newCell.setAttribute('valign', 'middle'); | |
newCell.setAttribute('nowrap', 'nowrap'); | |
var font = document.createElement('font'); | |
font.setAttribute('size', '2'); | |
font.appendChild(document.createTextNode(text)); | |
var nobr = document.createElement('nobr'); | |
nobr.appendChild(font); | |
var a = document.createElement('a'); | |
a.href = 'javascript:void(0);'; | |
a.addEventListener('click', func, true); | |
a.appendChild(nobr); | |
newCell.appendChild(a); | |
} | |
function changeSelectState(state) { | |
var inputs = document.getElementsByTagName('input'); | |
for (var i = 0; i < inputs.length; i++) { | |
var input = inputs[i]; | |
var evt = document.createEvent('MouseEvents'); | |
if (input.type == 'checkbox' && input.checked != state) { | |
// this won't fire the events to color the row | |
// input.checked = true; | |
// so dispatch as an event instead | |
evt.initEvent('click', true, false); | |
input.dispatchEvent(evt); | |
} | |
} | |
} | |
function addSelectAll(toolbarTable) { | |
addToolbarButton(toolbarTable, 'Select All', function() { | |
changeSelectState(true); | |
}); | |
} | |
function addSelectNone(toolbarTable) { | |
addToolbarButton(toolbarTable, 'Select None', function () { | |
changeSelectState(false); | |
}); | |
} | |
function setProps(sXml, loadFunc, errorFunc) { | |
GM_xmlhttpRequest({ | |
method: 'BPROPPATCH', | |
url: getBaseHref(), | |
headers: { | |
'Accept-Language': 'en-us', | |
'Content-Type': 'text/xml' | |
}, | |
data: sXml, | |
onload: loadFunc, | |
onerror: errorFunc | |
}); | |
} | |
function changeReadStatus(status) { | |
var selInputs = []; | |
var inputs = document.getElementsByTagName('input'); | |
for (var i = 0; i < inputs.length; i++) { | |
var input = inputs[i]; | |
if (input.type == 'checkbox' && input.checked) selInputs.push(input); | |
} | |
if (selInputs.length > 0) { | |
var sFlags = 'd:flags="1"'; // suppress receipt | |
var sXml = '<?xml version="1.0"?\><a:propertyupdate xmlns:a="DAV:"><a:set><a:prop><d:read xmlns:d="urn:schemas:httpmail:" ' + sFlags + '>'; | |
sXml += (status) ? '1':'0'; | |
sXml += '</d:read></a:prop></a:set><a:target>'; | |
for (var i = 0; i < selInputs.length; i++) { | |
var input = selInputs[i]; | |
sXml += '<a:href>' + getBaseHref() + input.value + '</a:href>'; | |
} | |
sXml += '</a:target></a:propertyupdate>'; | |
var loadFunc = function(res) { | |
var parser = new DOMParser(); | |
var doc = parser.parseFromString(res.responseText, 'text/xml'); | |
var responses = doc.getElementsByTagNameNS('DAV:', 'response'); | |
var baseHrefLength = getBaseHref().length; | |
for (var i = 0; i < responses.length; i++) { | |
var response = responses[i]; | |
var href = response.getElementsByTagNameNS('DAV:', 'href')[0]; | |
var id = href.firstChild.nodeValue.substring(baseHrefLength - 1); | |
for (var j = 0; j < selInputs.length; j++) { | |
var input = selInputs[j]; | |
if (input.type == 'checkbox' && input.value == id) { | |
var tr = input.parentNode.parentNode; | |
walkTheDom(tr, function(node) { | |
if (node.nodeType == 3) { | |
var text = node.nodeValue; | |
var parent = node.parentNode; | |
var grandParent = parent.parentNode; | |
if (status && parent.tagName == 'B') { | |
grandParent.removeChild(parent); | |
var font = document.createElement('font'); | |
font.size = 2; | |
font.color = 'black'; | |
font.appendChild(document.createTextNode(text)); | |
grandParent.appendChild(font); | |
} else if (!status && parent.tagName == 'FONT') { | |
grandParent.removeChild(parent); | |
var b = document.createElement('b'); | |
b.appendChild(document.createTextNode(text)); | |
grandParent.appendChild(b); | |
} | |
} else if (node.nodeType == 1 && node.tagName == 'IMG') { | |
if (status && node.src.match(UNREAD_IMG + '$')) { | |
node.src = READ_IMG; | |
} else if (!status && node.src.match(READ_IMG + '$')) { | |
node.src = UNREAD_IMG; | |
} | |
} | |
}); | |
} | |
} | |
} | |
}; | |
var errorFunc = function(res) { | |
alert('Change of read status failed, code: ' + res.status); | |
}; | |
setProps(sXml, loadFunc, errorFunc); | |
} | |
} | |
function addMarkRead(row) { | |
addToolbarButton(toolbarTable, 'R', function() { | |
changeReadStatus(true); | |
}); | |
} | |
function addMarkUnread(row) { | |
addToolbarButton(toolbarTable, 'U', function() { | |
changeReadStatus(false); | |
}); | |
} | |
var toolbarTable = getToolbarTable(); | |
if (toolbarTable) { | |
addSelectAll(toolbarTable); | |
addSelectNone(toolbarTable); | |
addMarkRead(toolbarTable); | |
addMarkUnread(toolbarTable); | |
} | |
// if this is the find names popup form let <enter> work as submit | |
var findNamesForm = getFindNamesForm(); | |
if (findNamesForm) { | |
var inputs = document.getElementsByTagName('input'); | |
for (var i = 0; i < inputs.length; i++) { | |
var input = inputs[i]; | |
if (input.type == 'text') { | |
input.addEventListener('keypress', function(evt) { | |
if (evt.keyCode == 13) findNamesForm.submit(); | |
}, true); | |
} | |
} | |
} | |
} | |
)(); |
Is there a keyboard shortcut to handle the Contact lookup when you type in the partial contact Name followed by Ctrl + K in the To/CC text fields when you compose a mail.
ReplyDeleteFirst of, you freaking rock if you pull this off. This is the ONLY reason why I am still tethered to IE/XP + Outlook.
ReplyDeleteThat said, I tried this and it didn't work on my setup:
OWA 2003
Firefox 3.0.1
The script installed fine, and Select All/None works. But marking read and unread doesn't change the state of the select messages (tried reloading, etc.)
I don't have a clue how to troubleshoot, but if you point me in the right direction, I am VERY interested in making this work...
well... that was easy.
ReplyDeleteYou had a bug on line 113 - it's hard-coded to _your_ OWA server. As a quick workaround, I changed it to my server and the script works great. I'll try to make a good fix and send it to you.
Thanks for this super cool script!
After a crash course in Greasemonkey and some really cool Firefox DOM inspection tools, that line should use a getBaseHref() ... but you obviously knew that since you wrote the getBaseHref function. ;)
ReplyDeleteThanks again - super cool!
BTW - do you mind if I continue to hack on this script? There may be some other things I want to do to OWA to make it work better.
deaston at sbcglobal dot net
@Dan - ugh, thanks for catching my error! I did in fact leave my hard coded URL in there when it should have been using the getBaseHref() function. I have corrected the code above and at the download link. I actually haven't been thinking about OWAX hacks lately because I switched to using fetchmail to forward my mail to a gmail account. Feel free to do anything with the code that you please. I'd be interested in knowing your improvements.
ReplyDeleteHi David,
ReplyDeleteI've been using your great userscript for over a year now in Firefox, and it has made dealing with my corporate e-mail a lot less painful. Thank you for that! So naturally, when Google Chrome got Greasemonkey support in its latest version, I immediately came over here and installed the script in Chrome. So this is just to let you know that it works fine in Chrome as well. Thanks again!
@Wieland - thanks for that update. I had read that Google Chrome was now supposed to run greasemonkey scripts but no longer have an Outlook Web Access account to test it against. Google must have done a heck of a job making Chrome greasemonkey compatible! Thats cool!
ReplyDeleteHi David, and thanks a lot for that script!
ReplyDeleteI was looking for the "mark as read/unread" feature for a long time.
It works great on my corporate OWA and Firefox 3.6
Great little script - thanks - the Read / unread feature makes reading my corporate mail much easier
ReplyDeleteGreat work! Helped me a lot. Had been looking for this for long.
ReplyDeletevery very nice! you're the man it works like a charm. thanx!
ReplyDeletecheers from Portugal!
It's pity that Mark as unread is not even there after opening a message in OWA.
ReplyDeleteShame on you Microsoft..
Terrific, thanks!
ReplyDeleteworks great! Thanks
ReplyDeleteAwesome, thanks a million!
ReplyDelete